<?php

// | Copyright (c) 2019~2020 http://jiunidu.com All rights reserved.
// +----------------------------------------------------------------------
// | Author: huanglijun <80792275@qq.com>
// +----------------------------------------------------------------------

namespace service\pay;

use think\facade\Log;
use think\facade\Request;
use service\tools\http\Curl;
use think\facade\Env;

class WxPay extends PayBase 
{ 
     //应用的ID
     protected $appId = null;
     //应用的密钥
     protected $appSecret = null;
     //商户的商户号
     protected $mchId =null;
     //商户的密钥
     protected $mchKey =null;
     //微信返回的统一下单的订单号，用于支付
     public $prepayId = null;
     //微信支付的类型
     public $trade_type              =   'JSAPI';
     //微信支付回调
     public $pay_notify_url             =   "";
     //微信退款支付回调
     public $refund_notify_url          =   "";
     //用户微信的open_id
     public $open_id                    =   "";
     //支付订单的附加信息
     public $attach                     =   "";
     //微信回调返回的数据
     public $request_data            =   [];
     //回调信息是否验证通过
     public $notice_result           =   true; 
     //验证回调信息结果
     public $notice_result_msg          =   '';

     public function __construct()
     {
         $this->appId =  Env::get('weixin.appid_home');
         $this->appSecret =Env::get('weixin.app_home_secret');
         $this->mchId = Env::get('weixin.app_home_merchantid');
         $this->mchKey = Env::get('weixin.app_home_merchantkey');
     }

     /**
     * 生成支付参数,带签名的
     * @return mixed
     */
     public function getPayParams()
     {
        $data = [
                    'timeStamp'=>(string)time(),
                    'nonceStr'=>$this->getNonceStr(),
                    'package'=>"prepay_id={$this->prepayId}",
                    'signType'=>'MD5',
                ];
        $signData = $data;
        $signData['appId'] = $this->appId;
        $data['paySign'] = $this->sign($signData);
        return $data;
     }

     /**
     *
     * 验证回调信息的签名
     * @return mixed
     */
     public function check_notify_sign()
     {
        libxml_disable_entity_loader(true);  //2018-07-05 爆出XXE漏洞
        $request = file_get_contents("php://input");
        Log::info("[wx notice] ip=".Request::ip().";data={$request}");
        try {
            $xml = @new \SimpleXMLElement($request);
            if (!isset($xml->return_code) || $xml->return_code != 'SUCCESS'
                || !isset($xml->result_code) || $xml->result_code != 'SUCCESS'
            ) 
            {
                throw new \Exception('参数格式校验错误1');
            }
            $requestData = array();
            foreach ($xml->children() as $k => $v) 
            {
                $requestData[$k] = $v->__toString();
            }
            if (!isset($requestData['out_trade_no']) || !$requestData['out_trade_no']
                || !isset($requestData['sign']) || !$requestData['sign']
            ) 
            {
                throw new \Exception('参数格式校验错误2');
            }
            $signData = $requestData;
            unset($signData['sign']);
            if(strtoupper($requestData['sign']) != $this->sign($signData))
            {
                throw new \Exception('签名失败');
            }
            $this->request_data=$requestData;
            $this->setOrderId($requestData['out_trade_no']);
            $this->setPayFee($requestData['total_fee']);
            $this->setTransactionId($requestData['transaction_id']);
            return true;
        } catch (\Exception $e)
        {
            $this->notice_result = false;
            $this->notice_result_msg = $e->getMessage();
            return false;
        }
     }

    /**
     * 处理第三方支付回调的时返回的数据
     * @return mixed
     */
     public function return_notify_info()
     {
        $return = "<xml>
                    <return_code><![CDATA[%s]]></return_code>
                    <return_msg><![CDATA[%s]]></return_msg>
                   </xml>";
        $return_code = $this->notice_result?'SUCCESS':'FAIL';
        Log::info("[wx notice] return_code={$return_code},return_msg={$this->notice_result_msg}");
        echo sprintf($return,$return_code,$this->notice_result_msg);
        exit(0);
     }

    /**
     * 微信支付统一下单接口
     * @return bool|string|null
     */
    public function unifiedOrder() 
    {
        $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        //统一下单的参数
        $postData = [
                    'appid'=>$this->appId,
                    'mch_id'=>$this->mchId,
                    'nonce_str'=>$this->getNonceStr(),
                    'sign_type'=>'MD5',
                    'body'=>$this->order_body,
                    'out_trade_no'=>$this->order_id,
                    'fee_type'=>'CNY',
                    'total_fee'=>$this->pay_fee,
                    'spbill_create_ip' => Request::ip(),
                    'notify_url' =>  $this->pay_notify_url ,
                    'trade_type' => $this->trade_type,
                    'openid' => $this->open_id,
                    'attach' => $this->attach,
                    ];

        $postData['sign'] = $this->sign($postData);
        $postDataXML = $this->createXMLStr($postData);
        $curl = new Curl();
        $data = $curl->request(
            $url,$postDataXML,'POST',false,array('Content-Type: text/xml')
        );
        if($data['httpcode'] === 200) 
        {
            $xml = new \SimpleXMLElement($data['data']);
            Log::info($data['data']);
            if(isset($xml->return_code) && $xml->return_code == 'SUCCESS'
                && isset($xml->result_code) && $xml->result_code == 'SUCCESS')
            {
                $this->prepayId = $xml->prepay_id->__toString();
                $return['prepayId']=  $this->prepayId;
                return $return;
            }
        }
        return false;
    }

    /**
     * 申请退款,统一下单接口
     * @return bool|string|null  微信退款单号
     */
    public function unifiedOrderRefund()
    {
        Log::info('RefundNotify backend:unifiedOrderRefund start');
    
        $url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
        //退款的参数
        $postData =[
                    'appid'=>$this->appId,
                    'mch_id'=>$this->mchId,
                    'nonce_str'=>$this->getNonceStr(),
                    'sign_type'=>'MD5',
                    'out_trade_no'=>$this->order_id,
                    'out_refund_no'=>$this->refund_id,
                    'total_fee'=>$this->pay_fee,
                    'refund_fee'=>$this->refund_fee,
                    'refund_fee_type'=>'CNY',
                    'notify_url' => $this->refund_notify_url,
                    ];

        $postData['sign'] = $this->sign($postData);
        $postDataXML = $this->createXMLStr($postData);
        $curl = new Curl();
        $curl->setSslCert(dirname(__DIR__).'/config/wxcert/apiclient_cert.pem');
        $curl->setSslKey(dirname(__DIR__).'/config/wxcert/apiclient_key.pem');
        $data = $curl->request(
            $url,$postDataXML,'POST',false,array('Content-Type: text/xml')
        );

        if($data['httpcode'] === 200) 
        {
            $xml = new \SimpleXMLElement($data['data']);
            if(isset($xml->return_code) && $xml->return_code == 'SUCCESS'
                && isset($xml->result_code) && $xml->result_code == 'SUCCESS')
            {
                return $xml->refund_id->__toString();
            } else {
                print_r($data);exit;
                return $xml->err_code_des->__toString();
            }
        }
        Log::info('RefundNotify backend:unifiedOrderRefund end');
        return false;
    }

    /**
     * 退款结果通知验证并处理数据
     * @return bool|mixed
     */
    public function check_refund_notify() 
    {
        libxml_disable_entity_loader(true);             //2018-07-05 爆出XXE漏洞
        $request = file_get_contents("php://input");
        Log::info("[wx notice refund] ip=".Request::ip().";data={$request}");
        try {
            $xml = @new \SimpleXMLElement($request);
            if (!isset($xml->return_code) || $xml->return_code != 'SUCCESS') 
            {
                throw new \Exception('参数格式校验错误1');
            }
            $requestData = array();
            foreach ($xml->children() as $k => $v) 
            {
                $requestData[$k] = $v->__toString();
            }
            if(!isset($requestData['req_info']) || !$requestData['req_info']) 
            {
                throw new \Exception('参数格式校验错误2');
            }
            $reqInfo = $this->refund_decrypt($requestData['req_info'],$this->mchKey);
            Log::info("[wx notice refund decrypt] req_info={$reqInfo}");
            if(!$reqInfo) throw new \Exception('解密错误');
            $infoXml = @new \SimpleXMLElement($reqInfo);
            $refundData = array();
            foreach ($infoXml->children() as $k => $v) 
            {
                $refundData[$k] = $v->__toString();
            }
            if(isset($refundData['refund_status']) && $refundData['refund_status'] == 'SUCCESS') 
            {
                $this->request_data=$refundData;
                $this->setRefundId($refundData['out_refund_no']);
                $this->setOrderId($refundData['out_trade_no']);
                $this->setRefundFee($refundData['refund_fee']);
                $this->setPayFee($refundData['total_fee']);
            }
            return true;
        } 
        catch (\Exception $e)
        {
            $this->notice_result = false;
            $this->notice_result_msg = $e->getMessage();
            return false;
        }
    }

    /**
     * 签名
     * @param array $postData   
     * @return string
     */
    public function sign(array $postData)
    {
        $str = '';
        ksort($postData);
        foreach($postData as $k=>$v){
            if($v!==''){
                $str.= "{$k}={$v}&";
            }
        }
        $str.="key={$this->mchKey}";
        return strtoupper(md5($str));
    }

    /**
     * 根据参数创建微信支持的XML
     * @param array $params
     * @return mixed
     */
    public function createXMLStr(array $params)
    {
        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><xml></xml>');
        foreach($params as $k=>$v)
        {
            $xml->addChild($k,$v);
        }
        return $xml->asXML();
    }

    /**
     * 退款数据解密
     * @param string $str       加密的数据
     * @param string $key       加密的密钥
     * @return bool|string
     */
    function refund_decrypt($str, $key) 
    {
        $data = base64_decode($str,true);
        return openssl_decrypt($data, 'AES-256-ECB', md5($key), OPENSSL_RAW_DATA);
    }

     /**
     * 生成随机数
     * @param string $pre  前缀 
     */
     public function getNonceStr($pre = 'JND')
     {
        return uniqid($pre);
    }
}